/**
 * Copyright (c) 2008, Andrew Carter All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer. Redistributions in binary
 * form must reproduce the above copyright notice, this list of conditions and
 * the following disclaimer in the documentation and/or other materials provided
 * with the distribution. Neither the name of Andrew Carter nor the names of
 * contributors may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */
package com.acarter.composabletree;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Vector;

import javax.swing.event.TreeModelEvent;
import javax.swing.event.TreeModelListener;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;

/**
 * 
 * @author Andrew Carter
 */
public class ComposableTreeModel implements TreeModel {

	private Vector<TreeModelListener> listeners;

	private HashMap<Class<?>, ComposableTreeNode> nodeTypes;

	private HashMap<Class<?>, Vector<ComposableTreeNode>> childrenNodeTypes;

	private Object root;

	/**
	 * Constructor.
	 * 
	 * @param root
	 */
	public ComposableTreeModel(Object root) {

		this.root = root;

		listeners = new Vector<TreeModelListener>();
		nodeTypes = new HashMap<Class<?>, ComposableTreeNode>();
		childrenNodeTypes = new HashMap<Class<?>, Vector<ComposableTreeNode>>();
	}

	/**
	 * 
	 * @param c
	 * @param nodeTypes
	 */
	public void addNodeType(ComposableTreeNode nodeType, Vector<ComposableTreeNode> childrenNodes) {

		nodeTypes.put(nodeType.getNodeClassType(), nodeType);
		childrenNodeTypes.put(nodeType.getNodeClassType(), childrenNodes);
	}

	/**
	 * 
	 * @param c
	 */
	public void removeNodeType(Class<?> c) {

		nodeTypes.remove(c);
		childrenNodeTypes.remove(c);
	}

	/**
	 * 
	 * @param c
	 * @return
	 */
	public ComposableTreeNode getNodeType(Class<?> c) {

		return nodeTypes.get(c);
	}

	/**
	 * 
	 * @param c
	 * @return
	 */
	public Vector<ComposableTreeNode> getChildNodeTypes(Class<?> c) {

		return childrenNodeTypes.get(c);
	}

	/**
	 * 
	 * @param c
	 * @return
	 */
	public Class<?> findRegisteredClass(Class<?> c) {

		Class<?> match = c;

		if(c != null) {

			Vector<ComposableTreeNode> children = childrenNodeTypes.get(match);

			if(children == null) {

				while((match = match.getSuperclass()) != null) {

					if((children = childrenNodeTypes.get(match)) != null)
						break;
				}
			}
		}

		return match;
	}

	/**
	 * Returns the child of parent at the index in the parent's child array.
	 * Parent must be a node previously obtained from this data source. This
	 * should not return null if index is a valid index for parent (that is
	 * index >= 0 && index < getChildCount(parent)).
	 */
	public Object getChild(Object parent, int index) {

		Class<?> registeredClass = findRegisteredClass(parent.getClass());

		if(registeredClass != null) {

			if(index >= 0) {

				Vector<ComposableTreeNode> childrenTypes = childrenNodeTypes.get(registeredClass);

				if(childrenTypes != null) {

					int indexAccumulator = 0;

					for(ComposableTreeNode nodeType : childrenTypes) {

						int childCount = nodeType.getChildCount(parent);

						if((index >= indexAccumulator) && (index < indexAccumulator + childCount))
							return nodeType.getChild(parent, index - indexAccumulator);

						indexAccumulator += childCount;
					}
				}
			}
		}

		return null;
	}

	/**
	 * Returns the number of children of parent. Returns 0 if the node is a leaf
	 * or if it has no children. parent must be a node previously obtained from
	 * this data source.
	 */
	public int getChildCount(Object parent) {

		Class<?> registeredClass = findRegisteredClass(parent.getClass());

		if(registeredClass != null) {

			Vector<ComposableTreeNode> childrenTypes = childrenNodeTypes.get(registeredClass);

			if(childrenTypes != null) {

				int children = 0;

				for(ComposableTreeNode nodeType : childrenTypes)
					children += nodeType.getChildCount(parent);

				return children;
			}
		}

		return 0;
	}

	/**
	 * Returns the index of child in parent. If either parent or child is null,
	 * returns -1. If either parent or child don't belong to this tree model,
	 * returns -1.
	 */
	public int getIndexOfChild(Object parent, Object child) {

		Class<?> registeredClass = findRegisteredClass(parent.getClass());

		if(registeredClass != null) {

			Vector<ComposableTreeNode> childrenTypes = childrenNodeTypes.get(registeredClass);

			if(childrenTypes != null) {

				int indexAccumulator = 0;

				for(ComposableTreeNode nodeType : childrenTypes) {

					int index = nodeType.getChildIndex(parent, child);

					if(index != -1)
						return index + indexAccumulator;

					indexAccumulator += nodeType.getChildCount(parent);
				}
			}
		}

		return -1;
	}

	/**
	 * Returns the parent of this child (only if this child is aware of its
	 * parent).
	 * 
	 * @param child
	 *            The child of the desired parent
	 * @return The parent of the child, or null
	 */
	public Object getParent(Object child) {

		Class<?> registeredClass = findRegisteredClass(child.getClass());

		if(registeredClass != null) {

			ComposableTreeNode nodeType = getNodeType(registeredClass);

			if(nodeType != null)
				return nodeType.getParent(child);
		}

		return null;
	}

	/**
	 * Returns the tree path from the given object back to the root. This
	 * requires that all nodes along the path be aware of their parent.
	 * 
	 * @param object
	 *            a child object
	 * @return a tree path from the root to the object, or null
	 */
	public TreePath getPathToRoot(Object object) {

		if(object == null)
			return null;

		TreePath path = new TreePath(root);

		ArrayList<Object> list = new ArrayList<Object>();

		list.add(0, object);

		while((object = getParent(object)) != null) {

			if(object.equals(root))
				break;

			list.add(0, object);
		}

		for(Object child : list)
			path = path.pathByAddingChild(child);
		
		return path;
	}

	/**
	 * Returns the root of the tree. Returns null only if the tree has no nodes.
	 */
	public Object getRoot() {

		return root;
	}

	/**
	 * Returns true if node is a leaf. It is possible for this method to return
	 * false even if node has no children. A directory in a file system, for
	 * example, may contain no files; the node representing the directory is not
	 * a leaf, but it also has no children.
	 */
	public boolean isLeaf(Object object) {

		return getChildCount(object) > 0 ? false : true;
	}

	/**
	 * Messaged when the user has altered the value for the item identified by
	 * path to newValue. If newValue signifies a truly new value the model
	 * should post a treeNodesChanged event.
	 */
	public void valueForPathChanged(TreePath path, Object newValue) {

		// TODO
	}

	/**
	 * Adds a listener for the TreeModelEvent posted after the tree changes.
	 */
	public void addTreeModelListener(TreeModelListener l) {

		listeners.add(l);
	}

	/**
	 * Removes a listener previously added with addTreeModelListener.
	 */
	public void removeTreeModelListener(TreeModelListener l) {

		listeners.remove(l);
	}

	/**
	 * 
	 * @param e
	 */
	public void fireTreeNodesChanged(TreeModelEvent e) {

		for(TreeModelListener l : listeners)
			l.treeNodesChanged(e);
	}

	/**
	 * 
	 * @param e
	 */
	public void fireTreeNodesInserted(TreeModelEvent e) {

		for(TreeModelListener l : listeners)
			l.treeNodesInserted(e);
	}

	/**
	 * 
	 * @param e
	 */
	public void fireTreeNodesRemoved(TreeModelEvent e) {

		for(TreeModelListener l : listeners)
			l.treeNodesRemoved(e);
	}

	/**
	 * 
	 * @param e
	 */
	public void fireTreeStructureChanged(TreeModelEvent e) {

		for(TreeModelListener l : listeners)
			l.treeStructureChanged(e);
	}
}
